/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.binding.beanregister;

import java.beans.*;
import java.lang.reflect.*;
import java.util.*;
import ch.trackedbean.binding.beanSpec.*;
import ch.trackedbean.common.*;
import ch.trackedbean.util.*;

/**
 * Register holding beans and their clones.<br>
 * Offers method to clone a bean and to write the data of a clone back to the parent bean.
 * 
 * @author M. Hautle
 */
public class BeanRegister {
	/** Map mapping a name with a bean */
	private final HashMap<String, Object> namedBeans = new HashMap<String, Object>();

	/** Map mapping a bean with it's parent (key: Bean, value: Parent Bean) */
	private final IdentityHashMap<Object, Object> parent = new IdentityHashMap<Object, Object>();

	/**
	 * Associate the given bean with the given key
	 * 
	 * @param key The key of the bean
	 * @param bean The bean
	 * @return Previous bean associatet with this key or null
	 */
	public Object addBean(final String key, final Object bean) {
		return namedBeans.put(key, bean);
	}

	/**
	 * Returns the bean with the given key
	 * 
	 * @param key The key of the bean
	 * @return The bean with the given key or null
	 */
	public Object getBean(final String key) {
		return namedBeans.get(key);
	}

	/**
	 * Removes the bean with the given key
	 * 
	 * @param key The key of the bean
	 * @return The removed bean or null if nothing was done
	 */
	public Object removeBean(final String key) {
		final Object bean = namedBeans.remove(key);
		if (bean == null)
			return null;
		removeBeanMappings(bean);
		return bean;
	}

	/**
	 * Removes the given bean
	 * 
	 * @param bean The bean to remove
	 */
	public void removeBean(final Object bean) {
		final Iterator<Object> childs = namedBeans.values().iterator();
		// remove all occourences
		while (childs.hasNext())
			if (childs.next() == bean)
				childs.remove();
		removeBeanMappings(bean);
	}

	/**
	 * Removes all parent/child mappings of the given bean
	 * 
	 * @param bean The bean
	 */
	private void removeBeanMappings(final Object bean) {
		parent.remove(bean);
		final Iterator<Object> childs = parent.values().iterator();
		// remove all occourences
		while (childs.hasNext())
			if (childs.next() == bean)
				childs.remove();
	}

	/**
	 * Clones the given bean and registers it as parent of the returned clone
	 * 
	 * @param <T> The bean type
	 * @param bean The bean to clone
	 * @return A deep shalow clone of the given bean
	 * @throws CloneFailedException If something went wrong
	 */
	@SuppressWarnings("unchecked")
	public <T> T cloneAndRegisterBean(final T bean) throws CloneFailedException {
		if (bean == null)
			return null;
		final IdentityHashMap<Object, Object> cache = new IdentityHashMap<Object, Object>();
		final T clone = (T) cloneBean(bean, cache);
		cache.clear();
		parent.put(clone, bean);
		return clone;
	}

	/**
	 * Clones the given bean.
	 * 
	 * @param <T> The bean type
	 * @param bean The bean to clone
	 * @return A deep shalow clone of the given bean
	 * @throws CloneFailedException If something went wrong
	 */
	@SuppressWarnings("unchecked")
	public static <T> T cloneBean(final T bean) throws CloneFailedException {
		if (bean == null)
			return null;
		final IdentityHashMap<Object, Object> cache = new IdentityHashMap<Object, Object>();
		final T clone = (T) cloneBean(bean, cache);
		cache.clear();
		return clone;
	}

	/**
	 * Clones the given list.
	 * 
	 * @param list The source list
	 * @return The cloned list
	 * @throws CloneFailedException If something went wrong
	 */
	public static List cloneList(final List list) throws CloneFailedException {
		if (list == null)
			return null;
		return cloneList(list, new IdentityHashMap<Object, Object>());
	}

	/**
	 * The recursive cloning method.
	 * 
	 * @param bean The bean to clone
	 * @param cache Cache used to detect cycles
	 * @return A shalow clone of the given bean
	 * @throws CloneFailedException If something went wrong
	 */
	private static Object cloneBean(final Object bean, final IdentityHashMap<Object, Object> cache) throws CloneFailedException {
		if (bean == null)
			return null;
		// if the object was already cloned then return this clone
		Object copy = cache.get(bean);
		if (copy != null)
			return copy;
		try {
			final PropertyDescriptor[] props = Introspector.getBeanInfo(bean.getClass()).getPropertyDescriptors();
			if (props == null)
				throw new CloneFailedException("Bean cloning failed cause for the bean " + bean.getClass() + " were no PropertyDescriptors defined!");
			int i = 0;
			Method readM = null;
			Method writeM = null;
			// search the first writable property
			for (; i < props.length; i++) {
				if (props[i] instanceof IndexedPropertyDescriptor)
					continue;
				readM = props[i].getReadMethod();
				writeM = props[i].getWriteMethod();
				if (readM != null && writeM != null)
					break;
			}
			// return the original bean if no writable property was found
			if (readM == null || writeM == null)
				return bean;
			// clone current bean
			copy = bean.getClass().newInstance();
			// put into cache (creates same structure and avoids endlessloops....)
			cache.put(bean, copy);
			// clone first property
			if (readM.getReturnType() == List.class)
				writeM.invoke(copy, cloneList((List) readM.invoke(bean), cache));
			else
				writeM.invoke(copy, cloneBean(readM.invoke(bean), cache));
			// clone the other properties
			for (i++; i < props.length; i++) {
				if (props[i] instanceof IndexedPropertyDescriptor)
					continue;
				readM = props[i].getReadMethod();
				writeM = props[i].getWriteMethod();
				if (readM != null && writeM != null)
					if (readM.getReturnType() == List.class)
						writeM.invoke(copy, cloneList((List) readM.invoke(bean), cache));
					else
						writeM.invoke(copy, cloneBean(readM.invoke(bean), cache));
			}
			// reset bean status
			final IBeanStatus status = BeanStatusImpl.getBeanStatus(copy);
			if (status != null) {
				status.clearInitialValues();
				status.forceBeanStatus(BeanStatusFlag.UNCHANGED, null);
			}
		} catch (CloneFailedException e) {
			throw e;
		} catch (final Exception e) {
			throw new TechnicalException("Bean cloning failed!", e);
		}
		return copy;
	}

	/**
	 * Clones the given list.
	 * 
	 * @param list The source list
	 * @param cache Cache used to detect cycles
	 * @return The cloned list
	 * @throws CloneFailedException If something went wrong
	 */
	@SuppressWarnings("unchecked")
	private static List cloneList(final List list, final IdentityHashMap<Object, Object> cache) throws CloneFailedException {
		if (list == null)
			return null;
		final Object o = cache.get(list);
		if (o != null)
			return (List) o;
		final List clone = new ArrayList();
		for (int i = 0, cnt = list.size(); i < cnt; i++)
			clone.add(cloneBean(list.get(i), cache));
		return clone;
	}

	/**
	 * Copies the content of the given bean into it's parent bean (only first layer)
	 * 
	 * @param bean The bean to commit
	 * @return True if the commit was sucessfull
	 * @throws CommitFailedException If something went wrong
	 */
	public boolean commitBean(final Object bean) throws CommitFailedException {
		final Object dst = parent.remove(bean);
		namedBeans.remove(bean);
		if (dst == null)
			return false;
		updateBean(bean, dst, new IdentityHashMap<Object, Object>());
		return true;
	}

	/**
	 * Copies the properties from the source bean to the destination bean.<br>
	 * The bean states were also copied.
	 * 
	 * @param <T> The bean type
	 * @param src The source bean
	 * @param dst The destination bean
	 * @return The updated destination bean
	 */
	public static <T> T updateBean(T src, T dst) {
		return updateBean(src, dst, new IdentityHashMap<Object, Object>());
	}

	/**
	 * Copies the properties from the source bean to the destination bean.<br>
	 * The bean states were also copied.
	 * 
	 * @param <T> The bean type
	 * 
	 * @param src The source bean
	 * @param dst The destination bean
	 * @param cache Cache of the allready commited objects (used to handle multiple references on same instance)
	 * @return The updated bean
	 * @throws CommitFailedException If something went wrong
	 */
	@SuppressWarnings("unchecked")
	private static <T> T updateBean(final T src, final T dst, final IdentityHashMap<Object, Object> cache) throws CommitFailedException {
		try {
			// copy null values normaly
			if (src == null)
				return null;
			// if allready commited then return this instance
			final Object o = cache.get(src);
			if (o != null)
				return (T) o;
			cache.put(src, dst);
			final PropertyDescriptor[] props = Introspector.getBeanInfo(src.getClass()).getPropertyDescriptors();
			// simple object (no bean)?
			if (props == null || props.length == 0)
				return src;
			int i = 0;
			Method readM = null;
			Method writeM = null;
			// search the first writable property
			for (; i < props.length; i++) {
				if (props[i] instanceof IndexedPropertyDescriptor)
					continue;
				readM = props[i].getReadMethod();
				writeM = props[i].getWriteMethod();
				if (readM != null && writeM != null)
					break;
			}
			// no writable property found -> simple object
			if (readM == null || writeM == null)
				return src;

			Object srcValue = readM.invoke(src);
			Object dstValue = readM.invoke(dst);
			// is the property a list?
			if (readM.getReturnType() == List.class)
				updateList((List) srcValue, (List) dstValue, src, dst, readM, writeM, cache);
			else
				writeM.invoke(dst, updateBean(srcValue, dstValue, cache));
			// update the other properties
			for (i++; i < props.length; i++) {
				if (props[i] instanceof IndexedPropertyDescriptor)
					continue;
				readM = props[i].getReadMethod();
				writeM = props[i].getWriteMethod();
				if (readM == null || writeM == null)
					continue;
				srcValue = readM.invoke(src);
				dstValue = readM.invoke(dst);
				// is the property a list?
				if (readM.getReturnType() == List.class)
					updateList((List) srcValue, (List) dstValue, src, dst, readM, writeM, cache);
				else
					writeM.invoke(dst, updateBean(srcValue, dstValue, cache));
			}
		} catch (CommitFailedException e) {
			throw e;
		} catch (final Exception e) {
			throw new TechnicalException("Bean commit failed!", e);
		}
		return dst;
	}

	/**
	 * @param srcLst The source list
	 * @param dstLst The destination list
	 * @param src The source object (owning the source list)
	 * @param dst The destination object (owning the destination list)
	 * @param readM The read method of the property
	 * @param writeM The write method of the property
	 * @param cache Cache of the allready commited objects (used to handle multiple references on same instance)
	 * @throws InvocationTargetException If something went wrong
	 * @throws IllegalAccessException If something went wrong
	 * @throws IllegalArgumentException If something went wrong
	 */
	@SuppressWarnings("unchecked")
	private static void updateList(List srcLst, List dstLst, Object src, Object dst, Method readM, Method writeM, final IdentityHashMap<Object, Object> cache)
			throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
		if (srcLst == null || dstLst == null) {
			writeM.invoke(dst, srcLst);
			return;
		}
		// check if the list was allready commited
		Object cacheLst = cache.get(srcLst);
		if (cacheLst != null) {
			writeM.invoke(dst, cacheLst);
			return;
		}
		cache.put(srcLst, dstLst);
		dstLst.clear();
		for (Object o : srcLst) {
			final Object c = cache.get(o);
			dstLst.add(c != null ? c : o);
		}
	}

	/**
	 * Exception indicating that the bean commiting has failed
	 * 
	 * @author M. Hautle
	 */
	public static class CommitFailedException extends RuntimeException {

		/**
		 * Default constructor
		 * 
		 * @param message The detail message
		 */
		public CommitFailedException(final String message) {
			super(message);
		}

		/**
		 * Default constructor
		 * 
		 * @param message The detail message
		 * @param cause The cause
		 */
		public CommitFailedException(final String message, final Throwable cause) {
			super(message, cause);
		}
	}
}
